服务端架构
基于 rts-server-golang cmd/server/ 解析 关键词: 单体架构, Room注册表, 连接管理, 可观测性, Prometheus
概述
服务端采用单体架构(Monolithic):所有组件(网络、房间管理、录制)在一个进程内,通过函数调用通信。
这对于 RTS 游戏是合理的——对战房间数量少(不像 MMORPG 万人在线),瓶颈在 CPU/内存而不是分布式协调。
进程结构
main()
├─ 初始化 logger (slog)
├─ 初始化 metrics registry (obs)
├─ 初始化 room registry
├─ 启动 UDP listener
├─ 启动 retransmission ticker (每 20ms)
├─ 启动 HTTP server (/metrics, /trace)
└─ 启动 signal handler (SIGINT/SIGTERM)核心组件
1. Listener — UDP 网络层
go
listener, err := transport.Listen(transport.ListenerConfig{
Addr: ":9000",
Logger: log,
})
listener.OnNewConn = func(conn *transport.Conn) {
go handleConnection(conn, registry, defaults, log)
}
go listener.Serve()每个新连接启动一个 goroutine 处理。
2. Room Registry — 房间管理
go
registry := room.NewRegistry(defaults, log)
// GetOrCreate: 根据 roomID 获取或创建房间
rm := registry.GetOrCreate(roomID)
playerID, ok := rm.AddPlayer(conn)房间按需创建,人走房不自动销毁(由配置决定)。
3. Connection Handler — 连接生命周期
handleConnection(conn):
1. read Hello (超时 5s) → wire.Decode → MsgHello
2. send HelloAck
3. read JoinRoom (超时 5s) → wire.Decode → MsgJoinRoom
4. registry.GetOrCreate(roomID)
5. rm.AddPlayer(conn) → 分配 playerID
6. send JoinAck (含 Seed/MapW/MapH)
7. 循环: conn.Inbox → rm.Inbox (消息转发)4. Room — 游戏逻辑单元
每个房间独立运行一个 goroutine:
go
func (r *Room) Run(ctx context.Context) {
for {
// 定时 tick 循环(见 02_帧同步房间)
select {
case <-ctx.Done(): return
case msg := <-r.Inbox: r.handleMsg(msg)
}
}
}消息流向
客户端 UDP
↓
transport.Conn (可靠UDP处理)
↓ conn.Inbox
handleConnection goroutine
↓ rm.Inbox <- RoomMsg
Room.Run (帧同步逻辑)
├─ sim.Step(world, cmds)
├─ broadcast FrameBundle
├─ hashAgg.Report
└─ replayWriter.WriteTick
↓
transport.Conn.Send (广播给所有玩家)
↓
客户端可观测性
Metrics — Prometheus 风格
go
metrics.Counter("rts_udp_packets_in_total")
metrics.Counter("rts_udp_retransmit_total")
metrics.Histogram("rts_udp_rtt_ms")
metrics.Histogram("rts_tick_duration_ms")
metrics.Gauge("rts_rooms_active")
metrics.Gauge("rts_input_delay_n")
metrics.Counter("rts_desync_total")暴露在 GET /metrics。
Trace — 最近帧的 trace
go
trace := obs.NewTraceRing(obs.DefaultTraceSize)固定大小的环形 buffer,记录最近 N 帧的关键事件(tick 时间、seal 延迟、hash 状态)。暴露在 GET /trace。
日志 — slog
go
log := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))
log.Info("player joined", "player_id", playerID, "room", roomID)
log.Warn("DESYNC DETECTED", "tick", tick)启动参数
bash
./server \
-addr :9000 \ # UDP 监听地址
-http :9001 \ # HTTP 监控端口
-tick-rate 20 \ # 每秒帧数
-initial-n 3 \ # 初始输入延迟
-players 2 \ # 每房间人数
-seed 42 \ # 世界 seed
-map-w 100 -map-h 100 \ # 地图尺寸
-log-level info # 日志级别可靠性设计
| 设计 | 作用 |
|---|---|
| 单体架构 | 减少网络跳数,降低延迟 |
| 单 actor Room | 无锁并发,简化逻辑 |
| 录制写文件 | 崩溃后可恢复 |
| 独立 goroutine per 连接 | 连接隔离,一个崩了不影响其他 |
局限性
| 局限 | 说明 |
|---|---|
| 单机瓶颈 | 单机约 1000 并发房间(每房间 2 人) |
| 无法水平扩展 | 房间不能跨服务器 |
| 无分服合服 | 房间固定在创建服务器 |
对于 RTS 1v1/2v2 对战(最多 4 人),单机足够。大型 MMO RTS 需要分布式改造(房间调度、服务发现、跨服通信)。
相关
- 项目源码: rts-server-golang
/cmd/server/ - 上篇: 05_二进制协议设计
- 下篇: 07_测试与混沌测试